package com.heima.schedule.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.heima.common.constants.ScheduleConstants;
import com.heima.common.exception.CustomException;
import com.heima.common.redis.CacheService;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.schedule.dtos.Task;
import com.heima.model.schedule.pojos.Taskinfo;
import com.heima.model.schedule.pojos.TaskinfoLogs;
import com.heima.schedule.mapper.TaskLogsMapper;
import com.heima.schedule.mapper.TaskMapper;
import com.heima.schedule.service.TaskService;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import java.util.Date;
import java.util.List;
import java.util.Set;

/**
 * @author brianxia
 * @version 1.0
 * @date 2022-09-29 11:34
 */
@Service
@Slf4j
public class TaskServiceImpl implements TaskService {
    @Autowired
    private TaskMapper taskMapper;
    @Autowired
    private TaskLogsMapper taskLogsMapper;
    @Autowired
    private CacheService cacheService;

    /**
     * 新增任务
     * @param task
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public long addTask(Task task) {
        try{
            //1.存放到数据库
            long l = addTaskToDb(task);
            //2.存放到redis | list 和 zset
            task.setTaskId(l);
            addTaskToRedis(task);

            return l;
        }catch (Exception e){
            log.error("添加任务写入数据库和redis失败",e);
            throw e;
        }
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public void cancelTask(long taskId) {
        try{
            //1.存放到数据库
            Task task = cancelTaskToDb(taskId);
            //2.存放到redis | list 和 zset
            cancelTaskToRedis(task);

        }catch (Exception e){
            log.error("删除任务失败",e);
            throw e;
        }
    }

    @Override
    public Task poll(Integer type,Integer priority) {
        String key = generateRedisKey(ScheduleConstants.TOPIC, type, priority);
        //拉取数据
        String taskId = cacheService.lRightPop(key);
        if(taskId == null){
            return null;
        }

        String taskStr = (String)cacheService.hGet(ScheduleConstants.HASH_NAME, taskId);
        //同步删除hash中的数据
        cacheService.hDelete(ScheduleConstants.HASH_NAME, taskId);
        return JSON.parseObject(taskStr,Task.class);
    }

    private void cancelTaskToRedis(Task task) {
        //判断时间
        long executeTime = task.getExecuteTime();
        Date date = new Date();
        long currentTime = date.getTime(); //当前时间

        long time5Minutes = DateTime.now().plusMinutes(5).toDate().getTime(); //5分钟之后的时间
        String strTaskId = JSON.toJSONString(task.getTaskId());
        //已经到时间放list
        if(executeTime <= currentTime){
            //删除时，逻辑比对当前数据是否相当，假设list zset中有1W数据，比对时间会很长，如何提升比对效率
            cacheService.lRemove(generateRedisKey(ScheduleConstants.TOPIC,task.getTaskType(),task.getPriority()),
                  0,  strTaskId);
            cacheService.hDelete(ScheduleConstants.HASH_NAME,strTaskId);
        }else if(executeTime <= time5Minutes){
            //5分钟之内，未到时间放zset
            cacheService.zRemove(generateRedisKey(ScheduleConstants.FUTURE,task.getTaskType(),task.getPriority()),
                    strTaskId);
            cacheService.hDelete(ScheduleConstants.HASH_NAME,strTaskId);
        }
    }

    private Task cancelTaskToDb(long taskId) {
        taskMapper.deleteById(taskId);

        TaskinfoLogs taskinfoLogs = taskLogsMapper.selectById(taskId);
        taskinfoLogs.setStatus(ScheduleConstants.CANCELLED);

        int i = taskLogsMapper.updateById(taskinfoLogs);
        if(i == 0){
            throw new CustomException(AppHttpCodeEnum.DATA_EXIST);
        }
        Task task = new Task();
        BeanUtils.copyProperties(taskinfoLogs,task);
        task.setExecuteTime(taskinfoLogs.getExecuteTime().getTime());
        return task;
    }

    private String generateRedisKey(String prefix,Integer type,Integer priority){
        return prefix + type + "_" + priority;
    }

    private void addTaskToRedis(Task task) {
        //判断时间
        long executeTime = task.getExecuteTime();
        Date date = new Date();
        long currentTime = date.getTime(); //当前时间

        long time5Minutes = DateTime.now().plusMinutes(5).toDate().getTime(); //5分钟之后的时间
        //已经到时间放list
        if(executeTime <= currentTime){
            cacheService.lLeftPush(generateRedisKey(ScheduleConstants.TOPIC,task.getTaskType(),task.getPriority()),
                    JSON.toJSONString(task.getTaskId()));
            //cacheService.set(String.valueOf(task.getTaskId()),JSON.toJSONString(task)); hash内存占用少于string
        }else if(executeTime <= time5Minutes){
            //5分钟之内，未到时间放zset
            cacheService.zAdd(generateRedisKey(ScheduleConstants.FUTURE,task.getTaskType(),task.getPriority()),
                    JSON.toJSONString(task.getTaskId()),task.getExecuteTime());
        }

        if(executeTime <= time5Minutes){
            cacheService.hPut(ScheduleConstants.HASH_NAME,String.valueOf(task.getTaskId()),JSON.toJSONString(task));
            //将数据库中in_redis改成1代表数据在redis中存在
            taskMapper.update(null,Wrappers.<Taskinfo>lambdaUpdate()
                    .set(Taskinfo::getInRedis,1)
                    .eq(Taskinfo::getTaskId,task.getTaskId())
            );
        }

    }

    private long addTaskToDb(Task task) {
        //1.task表
        Taskinfo taskinfo = new Taskinfo();
        BeanUtils.copyProperties(task,taskinfo);
        taskinfo.setExecuteTime(new Date(task.getExecuteTime()));
        taskMapper.insert(taskinfo);

        //2.log表
        TaskinfoLogs taskinfoLogs = new TaskinfoLogs();
        BeanUtils.copyProperties(taskinfo,taskinfoLogs);
        taskinfoLogs.setVersion(1);
        taskinfoLogs.setStatus(ScheduleConstants.SCHEDULED);
        taskLogsMapper.insert(taskinfoLogs);

        return taskinfo.getTaskId();
    }

    /**
     * 同步zset数据到list
     */
    @Scheduled(cron = "0 * * * * ?") //启动两个实例，这个方法会别调用2次 xxl-job
    public void syncZsetToList(){
        String token = cacheService.tryLock("mylock106", 30 * 1000);
        if(token != null){
            System.out.println(new Date() + "调用同步zset数据到list");
            //获取所有zset名字
            Set<String> scan = cacheService.scan(ScheduleConstants.FUTURE + "*");
            long time = new Date().getTime();



            for (String s : scan) {

                String list = s.replace(ScheduleConstants.FUTURE,ScheduleConstants.TOPIC);
                Set<String> datas = cacheService.zRangeByScore(s, 0, time);
                cacheService.refreshWithPipeline(s,list,datas);
//            //获取到每一个zset名字
//
//            cacheService.lLeftPushAll(list,datas);
//            //从0到当前时间把所有数据删除掉
//            cacheService.zRemoveRangeByScore(s, 0, time);
            }
        }

    }

    @Scheduled(cron = "0 */3 * * * ?")
    @PostConstruct //ioc容器初始化阶段调用
    public void syncMysqlToRedis(){
        String token = cacheService.tryLock("mylockMysql106", 2* 60 * 1000);
        if(token != null){
            //从mysql查询出当前redis中不存在、时间在5分钟之内的数据
            List<Taskinfo> taskinfos = taskMapper.selectList(Wrappers.<Taskinfo>lambdaQuery()
                    .le(Taskinfo::getExecuteTime, DateTime.now().plusMinutes(5).toDate())
                    .eq(Taskinfo::getInRedis, 0)
            );

            //将数据同步到redis
            for (Taskinfo taskinfo : taskinfos) {
                Task task = new Task();
                BeanUtils.copyProperties(taskinfo,task);
                task.setExecuteTime(taskinfo.getExecuteTime().getTime());
                addTaskToRedis(task);
            }
        }
    }
}
